這 28 天一路寫下來,我發現自己最大的改變,不是突然記住了多少語法。
而是:
遇到問題時,我開始不會第一時間衝去改 Code。
以前看到 Bug,我很容易直接進入:
哪一行錯了?
↓
要改哪個 if?
↓
是不是補一個判斷就好?
但後來越來越常發現:
Bug 出現的位置,不一定就是 Root Cause 所在的位置。
有時候畫面錯了,真正原因在 API。
有時候 API 打錯了,真正原因在 Route。
有時候 Request 重複打,真正原因在 Component remount。
有時候 Input 顯示怪怪的,真正原因是兩個 State 在搶著控制它。
所以現在遇到 Bug,我開始先做一件事:
把流程畫出來。
以前看到:
「這裡有 Bug。」
我很容易直接看 Code。
但如果連 Bug 到底在什麼操作下發生都沒有確認,
其實很容易從錯的方向開始。
所以我現在會先問:
怎麼操作才會發生?
每次都發生嗎?
只有某些資料會發生嗎?
第一次正常、第二次才壞嗎?
重新整理後還會發生嗎?
例如:
進列表
↓
選 Filter
↓
進 Detail
↓
返回列表
↓
API 400
這和:
第一次進列表
↓
直接 API 400
其實是完全不同的問題。
前者很可能和:
URL 還原
State 恢復
Serialization
有關。
所以:
先把重現步驟寫清楚,本身就是 Debug 的第一步。
例如一個 Search:
使用者輸入
↓
Form
↓
Submit
↓
整理 Params
↓
Service
↓
Request
↓
Backend
↓
Response
↓
State
↓
Table
如果畫面出問題,
我就可以問:
到底是哪一站開始跟預期不一樣?
這比直接盯著 500 行 Component 有用很多。
很多 Bug 的核心其實是:
到底哪一份資料才是真的?
例如:
Form 裡有一份
State 裡有一份
URL 裡又有一份
API Response 又一份
假設畫面顯示:
ABC
我會開始問:
ABC 是從 Props 來的?
State?
Form?
URL?
還是 API Response?
只要找到:
Source of Truth
後面的資料流通常就比較容易追。
以前我很常只看:
value = FA
覺得:
值看起來沒錯啊。
但後來才發現:
"FA"
和:
["FA"]
人眼看起來很像。
程式來說卻完全不同。
所以現在除了問:
值是多少?
還會問:
它現在是 String?
Array?
Object?
null?
undefined?
尤其資料跨過:
Form
URL
Request
Response
這些邊界時,
型別很容易改變。
假設前端寫:
const payload = {
description: values.description
}
我不能只看:
console.log(payload)
然後就說:
有送啊。
因為真正重要的是:
瀏覽器最後送出去的 Request 到底是什麼?
所以我現在會看:
DevTools
↓
Network
↓
Request Payload
確認:
欄位真的存在嗎?
是 null?
是 ""?
還是根本被省略?
例如:
{
description: undefined
}
最後 JSON 可能根本沒有:
description
這種差異如果只看 UI,
很難發現。
假設:
PATCH 成功
Status 200
以前我很容易直接理解成:
修改成功。
但現在會拆開:
Request 有送對嗎?
↓
Backend 有接受嗎?
↓
真的有更新 Database 嗎?
↓
下一次 GET 回來是什麼?
因為:
200
只能告訴我:
這次 HTTP Request 被系統成功處理。
不一定代表:
我想改的欄位真的以我預期的方式被保存。
這招對我來說非常實用。
假設畫面顯示:
舊資料
我會先看 GET Response。
例如:
{
"name": "New Name"
}
但畫面還是:
Old Name
那問題比較可能在:
State
Form
Render
dataSource
Memo
也就是前端這一側。
例如:
{
"name": "Old Name"
}
那就不用一直改 React Render。
可以繼續往:
Backend
Database
Cache
追。
這個切法可以省很多亂猜的時間。
這幾天其實遇過很多這種例子。
表面:
API 打太多
真正原因可能是:
Component 被 unmount
↓
再 mount
↓
初始化 Request 又跑
表面:
Backend 收到 create 當 ID
真正原因可能是:
Route Match 錯
↓
create 被當成 :id
表面:
Backend 不接受參數
真正原因可能是:
Array
↓
URL
↓
String
↓
回來沒轉回 Array
所以我現在比較少問:
哪一支 API 壞了?
而會先問:
這個 Request 為什麼會變成現在這個樣子?
這可能是我現在最常用的 Debug 問題。
假設:
Input
↓
Form
↓
Payload
↓
API
↓
Response
↓
State
↓
UI
最後 UI 錯了。
不要一次看全部。
而是一站一站問:
Input 正確嗎?
↓
Yes
Form Value 正確嗎?
↓
Yes
Payload 正確嗎?
↓
No
那就停。
因為:
第一個錯誤已經出現在 Payload。
後面的:
API
Response
UI
即使全部錯,
也可能只是被前面的錯誤一路帶下去。
User Action
↓
UI Event
↓
State / Form
↓
Transform
↓
Request
↓
Backend
↓
Database
↓
Response
↓
State
↓
Render
然後:
✓
✓
✓
✗
找到第一個:
✗
通常就已經離 Root Cause 很近。
以前 Debug 很容易:
改 A
改 B
加 C
順便重構 D
最後:
欸,好了。
但這時候反而不知道:
到底是哪一個改動修好的?
所以現在我比較想做到:
提出一個假設
↓
只做最小修改
↓
重新測試
↓
確認假設成立或不成立
例如:
我懷疑 Filter 收合會造成 remount。
那先不要改 Request。
可以先:
加 mount / unmount log
確認:
真的有 remount 嗎?
如果沒有,
就把這個假設排除。
這比:
先加 cache 看看
更容易找到真正原因。
假設 API 重複打。
我可以:
加一個 flag
↓
第二次不要 Request
這可能:
功能看起來好了。
但如果真正原因是:
Component 不該一直 remount
那這個 flag 可能只是:
Workaround
而不是:
Root Fix
所以我現在會問:
這個修改是在修原因?
還是在壓症狀?
Workaround 不是永遠不能用。
有時候因為:
時程
風險
外部限制
確實需要先擋。
但至少要知道:
我現在做的是哪一種。
以前我會覺得 Debug 是:
找到錯誤 Code
↓
改掉
現在比較像:
建立模型
↓
找到實際行為和模型不一致的地方
↓
縮小範圍
↓
驗證原因
↓
選擇修法
所以 Debug 本身其實是一種:
推理過程。
如果遇到一個陌生 Bug,
我會先跑這份:
1. 怎麼穩定重現?
2. 正常流程應該怎麼走?
3. 誰觸發這件事?
4. 資料從哪裡來?
5. Source of Truth 是誰?
6. 每一站的值和型別是什麼?
7. Network Request 真正送了什麼?
8. Response 真正回了什麼?
9. 第一個錯誤在哪一層出現?
10. 我現在的修改是在修 Root Cause,
還是只是在擋症狀?
這十題不一定每次全部需要。
但當我完全不知道問題在哪裡時,
它可以幫我重新找到入口。
以前開 DevTools:
Console 有沒有紅字?
現在會分不同需求:
Console
→ 值、執行順序、Error
Network
→ Request / Response
React DevTools
→ Props / State / Component
Breakpoint
→ 程式到底怎麼走
工具沒有變。
改變的是:
我開始知道自己想確認什麼。
現在 Debug 當然可以問 AI。
例如把錯誤貼進去:
幫我修。
AI 很可能很快就給出一段可以試的 Code。
但如果我自己不知道:
這個資料從哪來
這支 API 為什麼打
這個 Component 為什麼 Render
那很容易變成:
AI 改一版
↓
測
↓
不行
↓
再貼回去
↓
再改一版
最後即使功能好了,
我還是不知道:
到底發生什麼事。
所以我現在比較希望 AI 幫我的方式是:
我先畫資料流
↓
提出目前假設
↓
讓 AI 幫我檢查盲點
↓
一起縮小範圍
而不是:
先把整份 Code 丟出去
↓
等答案
Day 01 的我比較容易問:
這一行是什麼?
現在遇到 Bug,
開始會問:
誰觸發?
資料從哪來?
中間被轉過幾次?
誰是 Source of Truth?
Request 真正送什麼?
Response 真正回什麼?
哪一層第一次出錯?
語法當然還是會有不懂的。
但現在我比較知道:
什麼時候真的需要查語法。
不是看到陌生 Code 就全部停下來。
而是先把流程抓住。
我現在的 Debug 流程大概是:
看到 Bug
↓
先重現
「什麼操作會發生?」
↓
畫正常流程
「資料應該怎麼走?」
↓
找 Source of Truth
「現在每一站實際是什麼?」
↓
檢查 Value + Type
↓
看 Network Request
「真的送出去什麼?」
↓
看 Response
「Server 真正回什麼?」
↓
找到第一個不符合預期的地方
↓
提出一個假設
↓
做最小修改驗證
↓
確認 Root Cause
↓
選擇 Root Fix / 合理的 Workaround
↓
Regression Test
最後還要再測:
原本的 Bug 好了嗎?
其他正常流程有沒有被我修壞?
因為:
修好一個 Case,不代表修改就結束了。
以前遇到 Bug:
看到錯
↓
改 Code
現在我更希望自己做到:
看到錯
↓
重現
↓
畫流程
↓
追資料
↓
找第一個錯誤
↓
驗證假設
↓
再改 Code
這中間看起來多了很多步驟。
但實際上,
它反而可能讓我少走很多冤枉路。
因為真正讓 Debug 變快的,
不一定是:
我打 Code 的速度變快。
而是:
我越來越快知道「不需要看哪裡」。
明天就是最後一天。
Day 30 我想回到這個系列一開始的問題:
30 天前,我以為看懂程式碼就是看懂每一行;30 天後,我開始知道真正要找的是那條把每一行串起來的線。